<?php

/**
 * Texy! - web text markup-language
 * --------------------------------
 *
 * Copyright (c) 2004, 2008 David Grudl aka -dgx- (http://www.dgx.cz)
 *
 * This source file is subject to the GNU GPL license that is bundled
 * with this package in the file license.txt.
 *
 * For more information please see http://texy.info/
 *
 * @copyright  Copyright (c) 2004, 2008 David Grudl
 * @license    GNU GENERAL PUBLIC LICENSE version 2 or 3
 * @link       http://texy.info/
 * @package    Texy
 */

// namespace Nette;


/**
 * NObject is the ultimate ancestor of all instantiable classes.
 *
 * It defines some handful methods and enhances object core of PHP:
 *   - access to undeclared members throws exceptions
 *   - support for conventional properties with getters and setters
 *   - support for event raising functionality
 *   - ability to add new methods to class (extension methods)
 *
 * Properties is a syntactic sugar which allows access public getter and setter
 * methods as normal object variables. A property is defined by a getter method
 * and optional setter method (no setter method means read-only property).
 * <code>
 * $val = $obj->label;     // equivalent to $val = $obj->getLabel();
 * $obj->label = 'Nette';  // equivalent to $obj->setLabel('Nette');
 * </code>
 * Property names are case-sensitive, and they are written in the camelCaps
 * or PascalCaps.
 *
 * Event functionality is provided by declaration of property named 'on{Something}'
 * Multiple handlers are allowed.
 * <code>
 * public $onClick;                // declaration in class
 * $this->onClick[] = 'callback';  // attaching event handler
 * if (!empty($this->onClick)) ... // are there any handlers?
 * $this->onClick($sender, $arg);  // raises the event with arguments
 * </code>
 *
 * Adding method to class (i.e. to all instances) works similar to JavaScript
 * prototype property. The syntax for adding a new method is:
 * <code>
 * function MyClass_prototype_newMethod(MyClass $obj, $arg, ...) { ... }
 * $obj = new MyClass;
 * $obj->newMethod($x); // equivalent to MyClass_prototype_newMethod($obj, $x);
 * </code>
 *
 * @author     David Grudl
 * @copyright  Copyright (c) 2004, 2008 David Grudl
 * @license    http://nettephp.com/license  Nette license
 * @link       http://nettephp.com/
 * @package    Nette
 */
abstract class NObject
{

    /**
     * Returns the name of the class of this object.
     *
     * @return string
     */
    final public function getClass()
    {
        return get_class($this);
    }



    /**
     * Access to reflection.
     *
     * @return ReflectionObject
     */
    final public function getReflection()
    {
        return new ReflectionObject($this);
    }



    /**
     * Call to undefined method.
     *
     * @param  string  method name
     * @param  array   arguments
     * @return mixed
     * @throws MemberAccessException
     */
    protected function __call($name, $args)
    {
        if ($name === '') {
            throw new MemberAccessException("Call to method without name.");
        }

        $class = get_class($this);

        // event functionality
        if (self::hasEvent($class, $name)) {
            $list = $this->$name;
            if (is_array($list) || $list instanceof Traversable) {
                foreach ($list as $handler) {
                    call_user_func_array($handler, $args);
                }
            }
            return;
        }

        // object prototypes support Class__method()
        // (or use class Class__method { static function ... } with autoloading?)
        $cl = $class;
        do {
            if (function_exists($nm = $cl . '_prototype_' . $name)) {
                array_unshift($args, $this);
                return call_user_func_array($nm, $args);
            }
        } while ($cl = get_parent_class($cl));

        throw new MemberAccessException("Call to undefined method $class::$name().");
    }



    /**
	 * Returns property value. Do not call directly.
     *
     * @param  string  property name
	 * @return mixed   property value
	 * @throws MemberAccessException if the property is not defined.
	 */
	protected function &__get($name)
	{
        if ($name === '') {
            throw new MemberAccessException("Cannot read an property without name.");
        }

        // property getter support
        $class = get_class($this);
        $m = 'get' . $name;
        if (self::hasAccessor($class, $m)) {
            // ampersands:
            // - using &__get() because declaration should be forward compatible (e.g. with NHtml)
            // - not using &$this->$m because user could bypass property setter by: $x = & $obj->property; $x = 'new value';
            $val = $this->$m();
            return $val;

        } else {
            throw new MemberAccessException("Cannot read an undeclared property $class::\$$name.");
        }
	}



	/**
	 * Sets value of a property. Do not call directly.
     *
	 * @param string  property name
	 * @param mixed   property value
     * @return void
     * @throws MemberAccessException if the property is not defined or is read-only
	 */
	protected function __set($name, $value)
	{
        if ($name === '') {
            throw new MemberAccessException('Cannot assign to an property without name.');
        }

        // property setter support
        $class = get_class($this);
        if (self::hasAccessor($class, 'get' . $name)) {
            $m = 'set' . $name;
            if (self::hasAccessor($class, $m)) {
                $this->$m($value);

            } else {
                throw new MemberAccessException("Cannot assign to a read-only property $class::\$$name.");
            }

        } else {
            throw new MemberAccessException("Cannot assign to an undeclared property $class::\$$name.");
        }
	}



	/**
	 * Is property defined?
     *
	 * @param string  property name
	 * @return bool
	 */
    protected function __isset($name)
	{
    	return $name !== '' && self::hasAccessor(get_class($this), 'get' . $name);
	}



    /**
     * Access to undeclared property.
     *
     * @param  string  property name
	 * @return void
     * @throws MemberAccessException
     */
    protected function __unset($name)
    {
        $class = get_class($this);
        throw new MemberAccessException("Cannot unset an property $class::\$$name.");
    }



    /**
	 * Has property an accessor?
     *
	 * @param  string  class name
     * @param  string  method name
	 * @return bool
	 */
    private static function hasAccessor($c, $m)
    {
        static $cache;
        if (!isset($cache[$c])) {
            // get_class_methods returns private, protected and public methods of NObject (doesn't matter)
            // and ONLY PUBLIC methods of descendants (perfect!)
            // but returns static methods too (nothing doing...)
            // and is much faster than reflection
            // (works good since 5.0.4)
            $cache[$c] = array_flip(get_class_methods($c));
        }
        // case-sensitive checking, capitalize the fourth character
        $m[3] = $m[3] & "\xDF";
        return isset($cache[$c][$m]);
    }



    /**
     * Is property an event?
     *
     * @param  string  class name
     * @param  string  method name
	 * @return bool
     */
    private static function hasEvent($c, $m)
    {
        return preg_match('#^on[A-Z]#', $m) && property_exists($c, $m);
    }

}
